<?php
/**
 * Mortar
 *
 * @copyright Copyright (c) 2009, Robert Hafner
 * @license http://www.mozilla.org/MPL/
 * @package OS
 * @subpackage ShellExec
 */

/**
 * ShellExec
 *
 * This class acts as a wrapper for shell commands- it locates binaries, escapes arguments, handles formating/displaying
 * of flags and options, and even handles piping output to files or other ShellExec instances.
 */
class ShellExec
{
	protected $longOpts = array();
	protected $shortOpts = array();
	protected $arguments = array();
	protected $flags = array();

	protected $pipeOutput;
	protected $outputFile;
	protected $truncate = true;

	protected $binary;

	protected $optionDelimiter = ' ';

	/**
	 * This function runs the command generated by getShellString using the shell_exec function and returns the
	 * results.
	 *
	 * @return string
	 */
	public function run(array $options = null)
	{
		if(isset($options['optionDelimiter']))
			$this->optionDelimiter = $options['optionDelimiter'];


		$command = $this->getShellString();
		return shell_exec($command);
	}

	/**
	 * This function can be used to set a path to an executable file or the name of a file to check in the current
	 * path for. In the event that a name is passed it can be bypassed using the constant "PATH_EXEC_$path".
	 *
	 * @param string $path Path or name of executable
	 * @return boolean False if unable to find binary
	 */
	public function setBinary($path)
	{
		if(!file_exists($path) || !is_executable($path))
			return false;

		$this->binary = $path;
		return true;
	}

	/**
	 * This function has the executable put its output directly into a file rather than having it get returned.
	 *
	 * @param string $file The file to output to
	 * @param boolean $truncate When true the file has its contents replaced rather than appended to the file
	 */
	public function setOutputFile($file, $truncate = true)
	{
		$this->outputFile = $file;
		$this->truncate = (bool) $truncate;
	}

	/**
	 * Adds options to the shell command
	 *
	 * @param string $argument
	 */
	public function addArgument($argument)
	{
		$this->arguments[] = $argument;
	}

	/**
	 * Adds flags (-s, -w, -g) to the shell command with an optional value. Any values passed are escaped using
	 * escapeshellarg
	 *
	 * @param string $flag
	 * @param string|ShellExec $value
	 */
	public function addFlag($flag, $value = null)
	{
		if(is_null($value))
		{
			$this->flags[] = $flag;
		}else{
			$this->shortOpts[$flag] = $value;
		}
	}

	/**
	 * Adds options (--list, --help, --version) to the shell command with an optional value. Any values passed are
	 * escaped using escapeshellarg
	 *
	 * @param string $option
	 * @param string|ShellExec $value
	 */
	public function addOption($option, $value = null)
	{
		if(is_null($value))
			$value = '';

		$this->longOpts[$option] = $value;
	}

	/**
	 * This function tells the class to output its results to another executable. ShellExec commands can be nested
	 * repeatedly using this function.
	 *
	 * @param ShellExec $pipeTo
	 */
	public function addPipe(ShellExec $pipeTo)
	{
		$this->pipeOutput = $pipeTo;
	}

	/**
	 * This function builds the shell command, including recursively building all passed ShellExec objects (through
	 * pipes or option/flag value).
	 *
	 * @return string Command to be run through exec or otherwise passed through to the system
	 */
	public function getShellString()
	{
		if(!isset($this->binary))
			throw new ShellExecError('No executable has been set to run.');

		$string = $this->binary;

		if(count($this->flags) > 0)
			$string .= ' -' . implode('', $this->flags);

		$string .= $this->buildOptions($this->shortOpts, '-');
		$string .= $this->buildOptions($this->longOpts, '--', $this->optionDelimiter);

		if(count($this->arguments) > 0)
			foreach($this->arguments as $argument)
				$string .= ' ' . self::escape($argument);

		if(isset($this->pipeOutput))
			$string .= ' | ' . $this->pipeOutput->getShellString();

		if(isset($this->outputFile))
		{
			$string .= ' ' . ($this->truncate ? '>' : '>>');
			$string .= ' ' . self::escape($this->outputFile);
		}

		return $string;
	}

	/**
	 * This function turns an array of options into a string.
	 *
	 * @param array $options (string) $name => (string) $option
	 * @param string $prefix Delimiter for option names, normally - or --
	 * @return string
	 */
	protected function buildOptions($options, $prefix, $delimiter = ' ')
	{
		$string = '';
		foreach($options as $name => $option)
		{
			$string .= ' ' . $prefix . $name;

			if($option instanceof ShellExec)
				$option = $option->getShellString();

			if(strlen($option) > 0)
				$string .= $delimiter . self::escape($option);
		}
		return $string;
	}

	/**
	 * This function returns numeric values intact while escaping other datatypes with escapeshellarg.
	 *
	 * @param mixed $value
	 * @return string
	 */
	static function escape($value)
	{
		if(is_numeric($value))
			return $value;

		return escapeshellarg($value);
	}
}

class ShellExecNotice extends CoreNotice {}
class ShellExecError extends CoreError {}
?>